package org.dftproject.genesis.ui.pages.circlediagram;

import org.dftproject.genesis.data.genealogy.Sex;
import org.dftproject.genesis.ui.pages.PageManager;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Cursors;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.FigureListener;
import org.eclipse.draw2d.FigureUtilities;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.MouseEvent;
import org.eclipse.draw2d.MouseListener;
import org.eclipse.draw2d.MouseMotionListener;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;

public class CircleDiagramFigure extends Figure {

	private final Font nameFont = JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT);
	private final Font summaryFont = JFaceResources.getDefaultFont();

	private final Color textColor = ColorConstants.black;
	private final Color borderColor = ColorConstants.white;
	private final Color deadEndColor = new Color(null, 0xAA, 0xAA, 0xAA);
	private final Color unsetColor = new Color(null, 0xCC, 0xCC, 0xCC);
	private final Color maleColor = new Color(null, 0xDD, 0xDD, 0xFF);
	private final Color femaleColor = new Color(null, 0xFF, 0xDD, 0xDD);
	private final Color selectedMaleColor = new Color(null, 0xBB, 0xBB, 0xFF);
	private final Color selectedFemaleColor = new Color(null, 0xFF, 0xBB, 0xBB);

	private final int minHorizontalMargin = 50;
	private final int minOuterArcLength = 10;
	private final int minScale = 20;

	private int scale;
	private int generations;

	private int hoverRow = -1;
	private int hoverColumn = -1;

	private CircleDiagramNode root;

	public CircleDiagramFigure() {
		addListeners();
	}

	/**
	 * Sets the person node that this circle diagram is focused on.
	 * @param node a node
	 */
	public void setPersonNode(CircleDiagramNode node) {
		root = node;
		repaint();
	}

	/**
	 * Sets the father node of a child node.
	 * @param child the node representing the child
	 * @param father the node representing the father
	 */
	public void setFatherNode(CircleDiagramNode child, CircleDiagramNode father) {
		if (father != null)
			father.setSex(Sex.Male);
		child.setLeft(father);
		repaint();
	}
	
	/**
	 * Sets the mother node of a child node.
	 * @param child the node representing the child
	 * @param mother the node representing the mother
	 */
	public void setMotherNode(CircleDiagramNode child, CircleDiagramNode mother) {
		if (mother != null)
			mother.setSex(Sex.Female);
		child.setRight(mother);
		repaint();
	}

	protected void setHover(int hoverRow, int hoverColumn) {
		if (this.hoverRow == hoverRow && this.hoverColumn == hoverColumn)
			return;
		int oldHoverRow = this.hoverRow;
		int oldHoverColumn = this.hoverColumn;
		this.hoverRow = hoverRow;
		this.hoverColumn = hoverColumn;
		repaint(getBounds(oldHoverRow, oldHoverColumn));
		repaint(getBounds(hoverRow, hoverColumn));
	}

	/**
	 * Returns the smallest bounding rectangle for the cell at the given row and
	 * column.
	 * @param row a row index
	 * @param column a column index
	 * @return the bounds of the requested cell
	 */
	protected Rectangle getBounds(int row, int column) {
		if (row < 0 || row >= generations)
			return new Rectangle();
		int columns = (int) Math.pow(2, row);
		if (column < 0 || column >= columns)
			return new Rectangle();

		Rectangle bounds;

		if (row == 0) {
			bounds = new Rectangle(-scale, -scale, 2 * scale, 2 * scale);

			CircleDiagramNode node = getNode(row, column);
			if (node != null)
				bounds.union(getTextBounds(node, new Point()));
		} else {
			int outerRadius = scale * (row + 1);
			int innerRadius = scale * row;

			double arcLength = -360.0 / columns;
			double arcOffset = -90.0 + arcLength * column;

			double theta = arcOffset * Math.PI / 180.0;
			Point p1 = new Point(outerRadius * Math.cos(theta), -outerRadius * Math.sin(theta));
			Point p2 = new Point(innerRadius * Math.cos(theta), -innerRadius * Math.sin(theta));

			theta = (arcOffset + arcLength / 2) * Math.PI / 180.0;
			Point p3 = new Point(outerRadius * Math.cos(theta), -outerRadius * Math.sin(theta));

			theta = (arcOffset + arcLength) * Math.PI / 180.0;
			Point p4 = new Point(outerRadius * Math.cos(theta), -outerRadius * Math.sin(theta));
			Point p5 = new Point(innerRadius * Math.cos(theta), -innerRadius * Math.sin(theta));

			int left = Math.min(p1.x, Math.min(p2.x, Math.min(p3.x, Math.min(p4.x, p5.x))));
			int top = Math.min(p1.y, Math.min(p2.y, Math.min(p3.y, Math.min(p4.y, p5.y))));
			int right = Math.max(p1.x, Math.max(p2.x, Math.max(p3.x, Math.max(p4.x, p5.x))));
			int bottom = Math.max(p1.y, Math.max(p2.y, Math.max(p3.y, Math.max(p4.y, p5.y))));

			bounds = new Rectangle(left, top, right - left + 1, bottom - top + 1);

			CircleDiagramNode node = getNode(row, column);
			if (node != null) {
				theta = (arcOffset + arcLength / 2) * Math.PI / 180.0;
				double middleRadius = scale * (row + 0.5);

				bounds.union(getTextBounds(node, new Point(middleRadius * Math.cos(theta), -middleRadius * Math.sin(theta))));
			}
		}

		return bounds.expand(1, 1).translate(getBounds().getCenter());
	}

	/**
	 * Returns the row and column of the cell containing the given coordinates.
	 * @param location coordinates
	 * @return the row and column of the cell
	 */
	protected int[] getCell(Point location) {
		Point coordinates = location.getTranslated(getBounds().getCenter().getNegated());

		double theta = Math.atan2(coordinates.y, coordinates.x) - Math.PI / 2;
		if (theta < 0)
			theta += 2 * Math.PI;
		double radius = Math.sqrt(coordinates.x * coordinates.x + coordinates.y * coordinates.y);

		int row = (int) radius / scale;
		int columns = (int) Math.pow(2, row);

		int column = (int) (columns * theta / (2 * Math.PI));
		if (column == columns)
			column = 0;

		return new int[] { row, column };
	}

	protected CircleDiagramNode getNode(int row, int column) {
		if (row < 0 || row >= generations || column < 0)
			return null;

		CircleDiagramNode node = root;
		while (row-- > 0 && node != null) {
			int halfColumns = (int) Math.pow(2, row);
			if (column < halfColumns) {
				node = node.getLeft();
			} else {
				column -= halfColumns;
				node = node.getRight();
			}
		}

		if (column > 0)
			node = null;
		return node;
	}

	protected boolean isCell(int row, int column) {
		return getNode(row, column) != null;
	}

	protected void addListeners() {
		MouseTracker tracker = new MouseTracker();
		addMouseMotionListener(tracker);
		addMouseListener(tracker);

		addFigureListener(new FigureListener() {

			public void figureMoved(IFigure source) {
				int radius = Math.min(getBounds().width - 2 * minHorizontalMargin, getBounds().height) / 2;

				generations = 2;
				while ((radius - radius / generations) * 2 * Math.PI / Math.pow(2, generations - 1) > minOuterArcLength && radius / generations > minScale) {
					++generations;
				}

				scale = radius / generations;
			}

		});
	}

	@Override
	public void paintFigure(Graphics graphics) {
		super.paintFigure(graphics);
		
		if (root != null) {
			graphics.pushState();
			graphics.translate(getBounds().getCenter());

			graphics.setLineWidth(2);
			graphics.setLineCap(SWT.CAP_SQUARE);
			paintNode(graphics, root, 0, 0);

			graphics.popState();
			graphics.pushState();
			graphics.translate(getBounds().getCenter());

			graphics.setForegroundColor(textColor);
			paintText(graphics, root, 0, 0);

			graphics.popState();
		}
	}

	protected void paintNode(Graphics graphics, CircleDiagramNode node, int row, int column) {
		if (row < generations - 1) {
			if (node.getLeft() != null)
				paintNode(graphics, node.getLeft(), row + 1, column * 2);
			if (node.getRight() != null)
				paintNode(graphics, node.getRight(), row + 1, column * 2 + 1);
		}

		graphics.pushState();

		int radius = scale * (row + 1);

		Rectangle bounds = new Rectangle(-radius, -radius, 2 * radius, 2 * radius);

		boolean hover = (row == hoverRow && column == hoverColumn);

		graphics.setBackgroundColor(node.getSex() == Sex.Male ? (hover ? selectedMaleColor : maleColor) : (hover ? selectedFemaleColor : femaleColor));

		if (row == 0) {
			graphics.setAntialias(SWT.OFF);
			graphics.fillOval(bounds);
			

			graphics.setAntialias(SWT.ON);

			if (!node.hasMore()) {
				graphics.setBackgroundColor(node.isLeftSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, 90, 180, 4);
				graphics.setBackgroundColor(node.isRightSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, 270, 180, 4);
			} else if (node.getLeft() == null) {
				graphics.setBackgroundColor(node.isLeftSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, 90, 180, 4);
			} else if (node.getRight() == null) {
				graphics.setBackgroundColor(node.isRightSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, 270, 180, 4);
			}

			graphics.setForegroundColor(borderColor);
			graphics.drawOval(bounds);
		} else {
			int columns = (int) Math.pow(2, row);
			double[] thetas = new double[2];

			float arcLength = -360.0f / columns;
			float arcOffset = -90.0f + arcLength * column;

			graphics.setAntialias(SWT.OFF);
			GraphicsUtils.fillArc(graphics, bounds, arcOffset, arcLength, scale);

			graphics.setAntialias(SWT.ON);
			
			if (!node.hasMore()) {
				graphics.setBackgroundColor(node.isLeftSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, arcOffset, arcLength / 2, 4);
				graphics.setBackgroundColor(node.isRightSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, arcOffset + arcLength / 2, arcLength / 2, 4);
			} else if (node.getLeft() == null) {
				graphics.setBackgroundColor(node.isLeftSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, arcOffset, arcLength / 2, 4);
			} else if (node.getRight() == null) {
				graphics.setBackgroundColor(node.isRightSet() ? deadEndColor : unsetColor);
				GraphicsUtils.fillArc(graphics, bounds, arcOffset + arcLength / 2, arcLength / 2, 4);
			}

			thetas[0] = arcOffset * Math.PI / 180.0;
			thetas[1] = (arcOffset + arcLength) * Math.PI / 180.0;

			graphics.setForegroundColor(borderColor);
			GraphicsUtils.drawArc(graphics, bounds, arcOffset, arcLength);

			int innerRadius = scale * row;
			for (int i = 0; i < thetas.length; i++) {
				Point p1 = new Point(radius * Math.cos(thetas[i]), -radius * Math.sin(thetas[i]));
				Point p2 = new Point(innerRadius * Math.cos(thetas[i]), -innerRadius * Math.sin(thetas[i]));
				graphics.drawLine(p1, p2);
			}
		}

		graphics.popState();
	}

	protected void paintText(Graphics graphics, CircleDiagramNode node, int row, int column) {
		if (row < generations - 1) {
			if (node.getLeft() != null)
				paintText(graphics, node.getLeft(), row + 1, column * 2);
			if (node.getRight() != null)
				paintText(graphics, node.getRight(), row + 1, column * 2 + 1);
		}

		boolean hover = (row == hoverRow && column == hoverColumn);
		if (!hover)
			return;

		if (row == 0) {
			paintText(graphics, node, new Point());
			return;
		}

		int columns = (int) Math.pow(2, row);

		double arcLength = -360.0 / columns;
		double arcOffset = -90.0 + arcLength * column;

		double theta = (arcOffset + arcLength / 2) * Math.PI / 180.0;
		double radius = scale * (row + 0.5);

		paintText(graphics, node, new Point(radius * Math.cos(theta), -radius * Math.sin(theta)));
	}

	protected void paintText(Graphics graphics, CircleDiagramNode node, Point center) {
		Dimension nameTextSize = FigureUtilities.getStringExtents(node.getName(), nameFont);

		Dimension summaryTextSize = new Dimension();

		String[] lines = node.getSummary().split("\n");
		for (String line : lines) {
			if ("".equals(line))
				continue;
			Dimension lineTextSize = FigureUtilities.getStringExtents(line, summaryFont);
			summaryTextSize.width = Math.max(summaryTextSize.width, lineTextSize.width);
			summaryTextSize.height += lineTextSize.height;
		}

		Dimension textSize = nameTextSize.getCopy();
		if (summaryTextSize != null) {
			textSize.width = Math.max(textSize.width, summaryTextSize.width);
			textSize.height += summaryTextSize.height;
		}

		graphics.setFont(nameFont);
		graphics.drawString(node.getName(), center.getTranslated(-nameTextSize.width / 2, -textSize.height / 2));

		int y = nameTextSize.height - textSize.height / 2;
		for (String line : lines) {
			if ("".equals(line))
				continue;
			Dimension lineTextSize = FigureUtilities.getStringExtents(line, summaryFont);
			graphics.setFont(summaryFont);
			graphics.drawString(line, center.getTranslated(-lineTextSize.width / 2, y));
			y += lineTextSize.height;
		}
	}

	protected Rectangle getTextBounds(CircleDiagramNode node, Point center) {
		Dimension nameTextSize = FigureUtilities.getStringExtents(node.getName(), nameFont);

		Dimension summaryTextSize = new Dimension();

		String[] lines = node.getSummary().split("\n");
		for (String line : lines) {
			if ("".equals(line))
				continue;
			Dimension lineTextSize = FigureUtilities.getStringExtents(line, summaryFont);
			summaryTextSize.width = Math.max(summaryTextSize.width, lineTextSize.width);
			summaryTextSize.height += lineTextSize.height;
		}

		Dimension textSize = nameTextSize.getCopy();
		if (summaryTextSize != null) {
			textSize.width = Math.max(textSize.width, summaryTextSize.width);
			textSize.height += summaryTextSize.height;
		}

		return new Rectangle(center, textSize).translate(-textSize.width / 2, -textSize.height / 2);
	}

	protected void handleActivate(MouseEvent me) {
		int[] cell = getCell(me.getLocation());
		CircleDiagramNode node = getNode(cell[0], cell[1]);
		if (node != null) {
			IWorkbench workbench = PlatformUI.getWorkbench();
			IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
			IWorkbenchPage workbenchPage = window.getActivePage();
			PageManager.getDefault().openPage(workbenchPage, new CircleDiagramPageInput(node.getPerson()), CircleDiagramPage.ID, true, (me.getState() & SWT.CTRL) != 0);
		}
	}

	protected class MouseTracker extends MouseMotionListener.Stub implements MouseListener {

		public void mousePressed(MouseEvent me) {
			requestFocus();
			me.consume();
		}

		public void mouseReleased(MouseEvent me) {
			if (getBounds().contains(me.getLocation())) {
				handleActivate(me);
			}
		}

		public void mouseDoubleClicked(MouseEvent me) {
		}

		@Override
		public void mouseEntered(MouseEvent me) {
			int[] cell = getCell(me.getLocation());
			setHover(cell[0], cell[1]);
			setCursor(isCell(cell[0], cell[1]) ? Cursors.HAND : Cursors.ARROW);
		}

		@Override
		public void mouseExited(MouseEvent me) {
			setHover(-1, -1);
			setCursor(Cursors.ARROW);
		}

		@Override
		public void mouseMoved(MouseEvent me) {
			int[] cell = getCell(me.getLocation());
			setHover(cell[0], cell[1]);
			setCursor(isCell(cell[0], cell[1]) ? Cursors.HAND : Cursors.ARROW);
		}

		@Override
		public void mouseDragged(MouseEvent me) {
			int[] cell = getCell(me.getLocation());
			setHover(cell[0], cell[1]);
			setCursor(isCell(cell[0], cell[1]) ? Cursors.HAND : Cursors.ARROW);
		}

	}

}
